매월 실시하고 있는 세일 결과를 살펴보니, 특정 App의 구매율이 다른 App에 비해 낮은 것이 확인되었다. 구매율이 낮은 원일을 조사해보니, 배너광고에 문제가 있을 수 있다는 것을 알게 되었다. 따라서 새 배너광고를 두 가지로 제작하여 구매율을 높이는데 어느쪽이 더 효율적인지를 검증하고자 한다.
먼저 현실과 이상적인 모습의 차이를 명확히 하기 위해 다른 앱과 해당 앱 간에 어떤 차이점이 존재하는지 가설을 세우도록 하자.
일단 이 정도의 가설을 세워놓고 1번 가설에 대한 다음과 같은 사실을 담당자로부터 확인하였다고 하자.
이것으로 첫번째 가설이 구매율에 영향을 미쳤을 가능성은 낮다고 볼 수 있다.
다음은 두번째 가설에 대한 추가정보이다.
아이템 세일의 배너광고는 해당 앱의 디자이너가 만들어 앱에 따라 품질이 제각각임
해당 앱의 배너광고는 항상 클릭률이 낮음
두번째 가설(배너광고의 표시내용에 문제가 있음)에 무게를 실어주는 정보이다. 따라서 배너광고의 클릭률을 높이는 것을 테마로 잡고 분석을 진행해보자.
데이터를 가져오기에 앞서 문제를 정리해보자.
문제
해결책
위의 해결책을 적용하려면 어떤 배너광고가 자주 클릭되는지 분명히 할 필요가 있다. 그런데 해당 앱에서는 지금 껏 매월 실시해온 아이템 세일의 배너광고를 한 번도 바꾸지않아 비교할 만한 데이터가 존재하지 않는다.
그래서 이번 사례에서는 2개의 배너광고를 새로 작성해서 어느 쪽이 더 나은지 데이터를 수집하는 것으로 한다.
전후비교로는 외부요인을 배제할 수 없음
2 개의 배너광고 중 어느 쪽이 더 나은지 조사하기 위해서는 어떻게 하면 좋을까? 같은 시기에는 A 배너 광고를 걸어놓고, 다른 시기에는 B 배너 광고를 내거는 전후비교라는 방법이 존재한다. 이 경우 A와 B를 비교하는 것은 A를 계속해서 내걸었을 경우에 예상되는 값과 B의 값을 비교하는 것이 된다. 하지만 이것으로 정말 올바르게 비교했다고 할 수 있을까? 이는 외부요인이 개입되었을 가능성을 배제할 수 없기 때문인데 예를 들어,
등과 같이 원래 비교하고자 했던 ‘배너 A’와 ’배너 B’ 이외의 외부요인의 영향을 받게된다. 이런 외부요인으로 인해 구매율이 좋아졌을 경우, 실제로 어떤 배너광고가 구매율 증가를 이끌어냈을지는 알 수 없게된다.
A/B 테스트로 외부요인 제거
이럴 때 편리한 검증 방법으로 A/B 테스트라는 것이 있다. A/B 테스트는 여러 선택지 중에서 어느 것이 가장 좋은 결과를 가져다줄지 알아보기 위한 검증 방법이다. A/B테스트는 초기 도입 시 개발비용이 많이 들지만 비교적 낮은 비용으로 실시할 수 있으며, 수집한 데이터를 통계적으로 취급하기 쉬워서 Web 업계에서 많이 이용하고 있다. 또한 일부 광고업이나 제조업 쪽에서도 실시하고 있다.
데이터의 구조를 파악한 뒤 처리하기 좋은 형태로 데이터 형을 바꿔주었다.
## log_date app_name test_name test_case
## Length:8598 Length:8598 Length:8598 Length:8598
## Class :character Class :character Class :character Class :character
## Mode :character Mode :character Mode :character Mode :character
##
##
##
## user_id transaction_id
## Min. : 2 Min. : 36
## 1st Qu.:20856 1st Qu.:20720
## Median :41675 Median :42886
## Mean :35687 Mean :42837
## 3rd Qu.:50442 3rd Qu.:63216
## Max. :57610 Max. :87920
## 'data.frame': 8598 obs. of 6 variables:
## $ log_date : chr "2013-10-01" "2013-10-01" "2013-10-01" "2013-10-01" ...
## $ app_name : chr "game-01" "game-01" "game-01" "game-01" ...
## $ test_name : chr "sales_test" "sales_test" "sales_test" "sales_test" ...
## $ test_case : chr "B" "B" "B" "B" ...
## $ user_id : int 15021 351 8276 1230 17471 48728 16929 30111 30328 32453 ...
## $ transaction_id: int 25638 25704 25739 25742 25743 25746 25769 25780 25791 25796 ...
# log_date -> 날짜형으로
# user_id -> 문자형으로
# transaction_id -> 문자형으로
# test_case -> 팩터형
# app_name -> 팩터형
ab_test_goal$log_date <- ab_test_goal$log_date %>%
ymd()
ab_test_goal$user_id <- ab_test_goal$user_id %>%
as.character()
ab_test_goal$transaction_id <- ab_test_goal$transaction_id %>%
as.character()
ab_test_goal$test_case <- ab_test_goal$test_case %>%
as.factor()
ab_test_goal$app_name <- ab_test_goal$app_name %>%
as_factor()
summary(ab_test_imp)## log_date app_name test_name test_case
## Length:87924 Length:87924 Length:87924 Length:87924
## Class :character Class :character Class :character Class :character
## Mode :character Mode :character Mode :character Mode :character
##
##
##
## user_id transaction_id
## Min. : 1 Min. : 1
## 1st Qu.:20869 1st Qu.:21982
## Median :42207 Median :43962
## Mean :35742 Mean :43962
## 3rd Qu.:50516 3rd Qu.:65943
## Max. :57610 Max. :87924
## 'data.frame': 87924 obs. of 6 variables:
## $ log_date : chr "2013-10-01" "2013-10-01" "2013-10-01" "2013-10-01" ...
## $ app_name : chr "game-01" "game-01" "game-01" "game-01" ...
## $ test_name : chr "sales_test" "sales_test" "sales_test" "sales_test" ...
## $ test_case : chr "B" "A" "B" "B" ...
## $ user_id : int 36703 44339 32087 10160 46113 6605 346 42710 37194 123 ...
## $ transaction_id: int 25622 25623 25624 25625 25626 25627 25628 25629 25630 25631 ...
## [1] 87924 6
# log_date -> 날짜형으로
# test_case -> 팩터형으로
# user_id -> 문자형으로
# transaction_id -> 문자형으로
# app_name -> 팩터형으로
# log_date app_name test_name test_case user_id transaction_id
# log_date app_name test_name test_case user_id transaction_id
ab_test_imp$log_date <- ab_test_imp$log_date %>%
ymd()
ab_test_imp$test_case <- ab_test_imp$test_case %>%
as.factor()
ab_test_imp$user_id <- ab_test_imp$user_id %>%
as.character()
ab_test_imp$transaction_id <- ab_test_imp$transaction_id %>%
as.character()
ab_test_imp$app_name <- ab_test_imp$app_name %>%
as.factor()데이터 전처리 과정으로 목적에 맞게 데이터를 결합하거나 추출하는 등의 작업이 포함되어 있다.
앞서 불러온 두 데이터를 결합하여 분석을 시작할 것이다. merge() 함수를 사용하여 ‘ab_test_goal’ 과 ‘ab_test_imp’를 ’transaction_id’를 기준으로 묶고 join 방식은 ’all.x=T(Left Outer Join)’을 사용할 것이다. 위에서 보면 중복되는 컬럼이 있는데 여기서 변수 이름이 중복되므로 suffixes = 옵션을 사용하여 중복되는 변수의 뒷 부분에 .g를 추가하였다. (참고 : suffixes = (’string’, ‘string’))
# 데이터 결합
ab_test_goal_imp <- merge(ab_test_imp, ab_test_goal, by ="transaction_id", all.x=T, suffixes =c("", ".g"))
head(ab_test_goal_imp)## transaction_id log_date app_name test_name test_case user_id log_date.g
## 1 1 2013-10-02 game-01 sales_test A 49017 <NA>
## 2 10 2013-10-02 game-01 sales_test A 10160 <NA>
## 3 100 2013-10-02 game-01 sales_test A 47937 <NA>
## 4 1000 2013-10-02 game-01 sales_test A 44583 <NA>
## 5 10000 2013-10-23 game-01 sales_test A 16356 <NA>
## 6 10001 2013-10-23 game-01 sales_test B 52347 <NA>
## app_name.g test_name.g test_case.g user_id.g
## 1 <NA> <NA> <NA> <NA>
## 2 <NA> <NA> <NA> <NA>
## 3 <NA> <NA> <NA> <NA>
## 4 <NA> <NA> <NA> <NA>
## 5 <NA> <NA> <NA> <NA>
## 6 <NA> <NA> <NA> <NA>
ab_test_goal_imp에 is.goal 이라는 항목이 추가되었다. 항목 user_id.g의 값이 ’NA’일 경우에는 ’0’을, 그 이외의 경우에는 ’1’을 기입하여 이것을 클릭했는지 하지 않았는지 판정하는 플래그로 삼는다.
# 클릭했는지 하지 않았는지 나타내는 플레그 작성
ab_test_goal_imp$is.goal <- ifelse(is.na(ab_test_goal_imp$user_id.g),0,1)
head(ab_test_goal_imp)## transaction_id log_date app_name test_name test_case user_id log_date.g
## 1 1 2013-10-02 game-01 sales_test A 49017 <NA>
## 2 10 2013-10-02 game-01 sales_test A 10160 <NA>
## 3 100 2013-10-02 game-01 sales_test A 47937 <NA>
## 4 1000 2013-10-02 game-01 sales_test A 44583 <NA>
## 5 10000 2013-10-23 game-01 sales_test A 16356 <NA>
## 6 10001 2013-10-23 game-01 sales_test B 52347 <NA>
## app_name.g test_name.g test_case.g user_id.g is.goal
## 1 <NA> <NA> <NA> <NA> 0
## 2 <NA> <NA> <NA> <NA> 0
## 3 <NA> <NA> <NA> <NA> 0
## 4 <NA> <NA> <NA> <NA> 0
## 5 <NA> <NA> <NA> <NA> 0
## 6 <NA> <NA> <NA> <NA> 0
클릭율을 집계하기 위해 plyr 패키지의 ddply 함수를 사용했다. ab_test_goal_imp 데이터의 test_case 항목별로 집계를 실시하고, 집계내용은 cvr 항목에 표시되며 ’클릭한 사람의 집계 / 배너광고가 표시된 유저수’로 클릭율을 산출한다.
# 클릭률 집계
library(plyr)
ddply(ab_test_goal_imp, .(test_case), summarize,
cvr = sum(is.goal)/length(user_id))## test_case cvr
## 1 A 0.08025559
## 2 B 0.11546015
카이제곱 검정에서는 chisq.test()함수를 사용하는데 여기서는 p-value < 2.2e-16 으로 매우 작게 나왔다.
##
## Pearson's Chi-squared test with Yates' continuity correction
##
## data: ab_test_goal_imp$test_case and ab_test_goal_imp$is.goal
## X-squared = 308.38, df = 1, p-value < 2.2e-16
ab_test_goal_imp 데이터의 log_date 와 test_case 단위로 세 번의 집계를 실시하였고, 첫 번째는 imp 항목에 user_id 수를 카운트해서 넣었다. 두 번째는 cv라는 항목에 is.goal 수의 합계를, 마지막으로 cvr 항목에는 이 두 수치를 나눈 비율을 넣었다. 즉, 그날 배너광고가 얼마나 표시되었고, 몇 명이 클릭했으며, 클릭율은 어떠한지 집계한 것이다. 그리고 summarize 대신에 transform을 사용했는데, 이것으로 집계한 결과를 원래 데이터에 추가할 수 있다. 여기서는 test_case별 클릭율을 원래 데이터에 추가하고 있다.
# 날짜별, 테스트 케이스별로 클릭율 산출
ab_test_goal_imp_summary <- ddply(ab_test_goal_imp, .(log_date, test_case), summarize,
imp = length(user_id),
cv = sum(is.goal),
cvr = sum(is.goal) / length(user_id))
# 테스트 케이스별로 클릭율 산출하기
ab_test_goal_imp_summary <- ddply(ab_test_goal_imp_summary, .(test_case), transform,
cvr.avg = sum(cv) / sum(imp))
head(ab_test_goal_imp_summary)## log_date test_case imp cv cvr cvr.avg
## 1 2013-10-01 A 1358 98 0.07216495 0.08025559
## 2 2013-10-02 A 1370 88 0.06423358 0.08025559
## 3 2013-10-03 A 1213 170 0.14014839 0.08025559
## 4 2013-10-04 A 1521 89 0.05851414 0.08025559
## 5 2013-10-05 A 1587 56 0.03528670 0.08025559
## 6 2013-10-06 A 1219 120 0.09844135 0.08025559
정제한 데이터를 가지고 시각화를 하는 단계이다.
library(scales)
ab_test_goal_imp_summary$log_date <- as.Date(ab_test_goal_imp_summary$log_date)
limits <- c( 0, max(ab_test_goal_imp_summary$cvr))
a <- ggplot(ab_test_goal_imp_summary, aes( x = log_date, y = cvr,
col = test_case, lty = test_case, shape = test_case)) +
geom_line(lwd =1) +
geom_point(size = 4) +
geom_line(aes( y = cvr.avg, col = test_case)) +
scale_y_continuous(label = percent, limits = limits)
ggplotly(a)이번에 시도해본 A/B 테스트 이외에 ‘어떤 두 개의 그룹에 차이가 있는지 없는지’ 조사하는 방법으로 통계학에는 ‘가설검정’이라는 기법이 있다. 그러나 가설검정은 ’샘플 사이즈가 작은 데 따른 오차를 고려하더라도 두 그룹 간의 차이가 있다고 할 수 있는가’ 를 확인하기 위한 수단에 그치기 때문에, 사람수가 많은 경우에는 대부분 ’통계적으로 유의한 차이가 있다’라는 결과가 된다. 사람수가 적은 데 따른 오차란, 예를 들어 두 그룹 모두 다섯 명으로 구성했을 때 결과적으로 두 그룹 간에 구매율 차이가 없다 하더라도, 그것이 다음과 같이 어느 한쪽 그룹의 누군가의 ’우연’에 의한 것일 수 있다는 것이다. 사람수가 적으면 한 명의 우연이 큰 영향을 끼치지만 사람수가 많으면 거의 영향을 끼치지 않는다. 따라서 가설검정은 사람수가 많아질수록 ’통계적으로 유의한 차이가 있다’는 결론을 내리기 쉬워지는 것이다. 그러나 ’통계적으로 유의한 차이’가 있어도 비즈니스에서 의미가 있는 차이라고 단정할 수는 없다. 특히 요즘같이 빅데이터 운운하는 시대에는 가설검정에서 다루는 오차가 매우 작어졌기 때문에 ’가설검정을 하지 않으면 통계적으로 차이가 있는지 없는지 알 수 없다’고 말할 일은 별로 없을 것이다. 그럼에도 불구하고 ’두 그룹간에 차이가 있어 보이지만 실은 통계적으로 유의한 차이는 아니였다’라고 한다면 곤란하므로 가설검증은 ’커트라인’으로 실시하는 것이다. 즉, ’가설검정에서 차이가 나타났기 때문에 이걸로 끝’이 아니라 ’적어도 가설검정에서는 의미가 있는 차이가 나타났으므로 이제 이게 비즈니스상에서 의미가 있는 차이인지 검토하자’라는 식으로 사용해야 한다는 것이다.